package commons.params_appli;

import commons.errors.ErrorsHandler;
import commons.logs.Logger;
import commons.utils.Utils;
import commons.utils.UtilsFile;
import java.awt.Color;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Vector;
import javax.naming.spi.DirectoryManager;
import javax.swing.plaf.metal.MetalIconFactory.FolderIcon16;


public abstract class A_ParamsAppli {

	protected static A_ParamsAppli paramsAppli=null;

	protected enum Condition {INTEGER_ANY, FLOAT_ANY, STRING_ANY,
							INTEGER_POS, INTEGER_POS_STRICT,
							INTEGER_BETWEEN,
							FLOAT_POS, FLOAT_POS_STRICT,
							FLOAT_BETWEEN, FLOAT_BETWEEN_STRICT,
							BOOL,
							FILE_NAME, FILE_EXTENSION, DIRECTORY_NAME_ABSOLUTE,
							FILE_EXISTANT, DIRECTORY_EXISTANT,
							FILE_TEXT_CHARSET,
							COLOR,
							LIST_VALUE}

	// un type de param dépend directement de sa condition (calculé automatiquement lors du register())
	private enum Type {STRING, INTEGER, FLOAT, BOOL, COLOR}

	// Des ParamConditions toutes prêtes
	protected static ParamCondition ParamCondition_String_notEmpty = new ParamCondition(Condition.STRING_ANY, false);
	protected static ParamCondition ParamCondition_String_canBeEmpty = new ParamCondition(Condition.STRING_ANY, true);
	protected static ParamCondition ParamCondition_Integer = new ParamCondition(Condition.INTEGER_ANY, false);
	protected static ParamCondition ParamCondition_Integer_Pos = new ParamCondition(Condition.INTEGER_POS, false);
	protected static ParamCondition ParamCondition_Integer_StrictPos = new ParamCondition(Condition.INTEGER_POS_STRICT, false);
	protected static ParamCondition ParamCondition_Float = new ParamCondition(Condition.FLOAT_ANY, false);
	protected static ParamCondition ParamCondition_Float_Pos = new ParamCondition(Condition.FLOAT_POS, false);
	protected static ParamCondition ParamCondition_Float_StrictPos = new ParamCondition(Condition.FLOAT_POS_STRICT, false);
	protected static ParamCondition ParamCondition_Bool = new ParamCondition(Condition.BOOL, false);
	protected static ParamCondition ParamCondition_FileName = new ParamCondition(Condition.FILE_NAME, false);
	protected static ParamCondition ParamCondition_FileExtension = new ParamCondition(Condition.FILE_EXTENSION, false);
	protected static ParamCondition ParamCondition_SubDirectory = new ParamCondition(Condition.FILE_NAME, false);
	protected static ParamCondition ParamCondition_Directory_Absolute = new ParamCondition(Condition.DIRECTORY_NAME_ABSOLUTE, false);
	protected static ParamCondition ParamCondition_File_Existant = new ParamCondition(Condition.FILE_EXISTANT, false);
	protected static ParamCondition ParamCondition_Directory_Existant = new ParamCondition(Condition.DIRECTORY_EXISTANT, false);
	protected static ParamCondition ParamCondition_FileText_Charset = new ParamCondition(Condition.FILE_TEXT_CHARSET, false);
	protected static ParamCondition ParamCondition_Color = new ParamCondition(Condition.COLOR, false);


	private INIFileReader iniFileReader;
	private HashMap<String, Object> mapValues;
	private HashMap<String, ParamCondition> mapConditions;


	public A_ParamsAppli() {
	}

	public void init (File fichierINI) {
		if (!fichierINI.exists()) {
			ErrorsHandler.addError("Le fichier de paramètre est introuvable :\n -> " + fichierINI);
			return ;
		}
		try {
			init(new FileInputStream(fichierINI));
		} catch (FileNotFoundException ex) {
			ErrorsHandler.addError(ex.getMessage());
			ErrorsHandler.addError("Impossible d'ouvrir le fichier de paramétrage :\n -> " + fichierINI);
		}
	}
	
	public void init (InputStream fichierINI) {
		assert(A_ParamsAppli.paramsAppli == null) : "Les paramètres de l'application ont déjà été lus !!";
		A_ParamsAppli.paramsAppli = this;

		try {
			iniFileReader = new INIFileReader();
			iniFileReader.load(fichierINI);

		}  catch (IOException ex) {
			ErrorsHandler.addError(ex.getMessage());
			ErrorsHandler.addError("Impossible de lire le fichier de paramétrage :\n -> " + fichierINI);
			return;
		} catch (Exception ex) {
			ErrorsHandler.addError(ex.getMessage());
			ErrorsHandler.addError("Impossible de lire le fichier de paramétrage :\n -> " + fichierINI);
			return;
		}	
		
		mapValues = new HashMap<String, Object>();
		//mapCategories = new Dictionary<String, String>();
		mapConditions = new HashMap<String, ParamCondition>();

		registerAll();
		if (ErrorsHandler.isThereErrors()) {
			ErrorsHandler.addError("Erreur lors du chargement du fichier de paramétrage de l'application :\n -> " + fichierINI);
			paramsAppli = null;
			return;
		}

		postTraitement();
		checkValidity();
		
		if (ErrorsHandler.isThereErrors()) {
			ErrorsHandler.addError("Erreur lors du chargement du fichier de paramétrage de l'application :\n -> " + fichierINI);
			paramsAppli = null;
		}
	}

	// A appeler en fin de registration de tous les paramètres, car le niveau de log a été fixé
	public void logLectureFichierINI() {
		if (iniFileReader != null)
			Logger.log_DEBUG1(iniFileReader.logDebug1);
	}

	abstract protected void registerAll();
	abstract protected void postTraitement();
	abstract protected void checkValidity(); // vérifications complémentaires à celles par défaut faites individuellement

	

	static public String getParam(String paramFullName) {
		paramFullName = paramFullName.toUpperCase();
		assert(paramsAppli.mapValues.containsKey(paramFullName)) : "Paramètre non déclaré : " + paramFullName;
		return paramsAppli.mapValues.get(paramFullName).toString();
	}
	static public int getParam_i(String paramFullName) {
		paramFullName = paramFullName.toUpperCase();
		assert(paramsAppli.mapValues.containsKey(paramFullName)) : "Paramètre non déclaré : " + paramFullName;
		assert(paramsAppli.mapConditions.get(paramFullName).type == Type.INTEGER) : "getParam_i() sur un paramètre de type non INTEGER : " + paramFullName;
		return (Integer)paramsAppli.mapValues.get(paramFullName.toUpperCase());
	}
	static public double getParam_d(String paramFullName) {
		paramFullName = paramFullName.toUpperCase();
		assert(paramsAppli.mapValues.containsKey(paramFullName)) : "Propriété non déclarée : " + paramFullName;
		assert(paramsAppli.mapConditions.get(paramFullName).type == Type.FLOAT) : "getParam_d() sur une propriété de type non FLOAT : " + paramFullName;
		return (Double)paramsAppli.mapValues.get(paramFullName);
	}
	static public Color getParam_c(String paramFullName) {
		paramFullName = paramFullName.toUpperCase();
		assert(paramsAppli.mapValues.containsKey(paramFullName)) : "Propriété non déclarée : " + paramFullName;
		assert(paramsAppli.mapConditions.get(paramFullName).type == Type.COLOR) : "getParam_c() sur une propriété de type non COLOR : " + paramFullName;
		return (Color)paramsAppli.mapValues.get(paramFullName);
	}
	static public boolean  getParam_b(String paramFullName) {
		paramFullName = paramFullName.toUpperCase();
		assert(paramsAppli.mapValues.containsKey(paramFullName)) : "Paramètre non déclaré : " + paramFullName;
		assert(paramsAppli.mapConditions.get(paramFullName).type == Type.BOOL) : "getParam_b() sur un paramètre de type non BOOL : " + paramFullName;
		return (Boolean) paramsAppli.mapValues.get(paramFullName);
	}


	static public void setParam(String paramFullName, Object val) {
		paramFullName = paramFullName.toUpperCase();
		assert(paramsAppli.mapValues.containsKey(paramFullName)) : "Propriété non déclarée : " + paramFullName;
		// on devrait tester le type, mais bon ...
		paramsAppli.mapValues.put(paramFullName, val);
	}


	// Déclare le paramètre, le lit dans le fichier INI, et vérifie sa validité
	protected void register(String paramFullName) {
		register(paramFullName, ParamCondition_String_notEmpty, null);
	}
	protected void register(String paramFullName, ParamCondition paramCondition) {
		register(paramFullName, paramCondition, null);
	}
	protected void register(String paramFullName, ParamCondition paramCondition, Object defaultValue) {
		paramFullName = paramFullName.toUpperCase();
		String[] t = Utils.mySplit(paramFullName, '.');
		assert (t.length <=2) : "il faut 1 \'.\' maximum dans le nom complet d'un paramètre";
		assert(!paramCondition.canBeEmpty() || defaultValue!=null) : "Paramètre pouvant être vide mais sans valeur par défaut : " + paramFullName;

		String categorie = (t.length==2) ? t[0] : "";
		String paramName = (t.length==2) ? t[1] : t[0];

		// Détection de doublon
		assert(!mapValues.containsKey(paramFullName)) : "Paramètre déclaré en doublon : " + paramFullName;

		String sVal = iniFileReader.getParam(categorie, paramName);
		if (sVal==null) {
			ErrorsHandler.addError("Paramètre non présent dans le fichier INI : [" + categorie + "] -> " + paramName);
			return;
		}

		// Traitement de la valeur par défaut
		if (sVal.length()==0 && paramCondition.canBeEmpty()) {
			if (defaultValue!=null) {
				mapConditions.put(paramFullName, paramCondition);
				mapValues.put(paramFullName, defaultValue);
				return;
			} else {
				// default value ne peut etre null, cf. assert du début
				assert(false);
				return;
			}
		}
		// Le cas val.length()==0 && pas canBeEmpty() est traité par checkValide() ci-après


		String msgError = paramCondition.checkValide(sVal);
		if (msgError != null) {
			ErrorsHandler.addError("Paramètre erroné : [" + categorie + "] -> " + paramName + " = " + sVal + "\n  => " + msgError);
			return;
		}

		Object oVal = transformeSValEnObject(sVal, paramCondition.type);
		mapValues.put(paramFullName, oVal);
		mapConditions.put(paramFullName, paramCondition);
	}


	// Les conversions ne peuvent pas échouer car checkValide() a vérifié cela avant
	private Object transformeSValEnObject(String val, Type type) {
		switch(type) {
			case STRING:
				return val;
			case FLOAT:
				return Double.parseDouble(val);
			case INTEGER:
				return Integer.parseInt(val);
			case BOOL:
				return val.equals("OUI");
			case COLOR:
				return decodeColor(val);
			default:
				assert(false) : "Type de paramètre incorrect : " + type;
				return null;
		}
	}



	protected static class ParamCondition {

		private Type type;
		private Condition condition;
		private boolean bCanBeEmpty;
		public boolean canBeEmpty() {return bCanBeEmpty;}

		// utilisé uniquement pour les conditions type xxx_BETWEEN_yyy
		private double borneMin=Double.NaN, borneMax=Double.NaN;

		// utilisé uniquement pour la condition LIST_VALUE
		private ArrayList<String> listeValue = null;

		public ParamCondition() {
			this(Condition.STRING_ANY, false);
		}
		public ParamCondition(Condition condition) {
			this(condition, false);
		}
		public ParamCondition(boolean bCanBeEmpty) {
			this(Condition.STRING_ANY, bCanBeEmpty);
		}
		public ParamCondition(Condition condition, boolean bCanBeEmpty) {
			this(condition, Double.NaN, Double.NaN, bCanBeEmpty);
		}
		public ParamCondition(String[] listeValue, boolean bCanBeEmpty) {
			this(Condition.LIST_VALUE, Double.NaN, Double.NaN, bCanBeEmpty);
			assert (listeValue!=null && listeValue.length>0) : "liste de valeur vide pour une condition LIST_VALUE";
			this.listeValue = new ArrayList(Arrays.asList(listeValue));
		}
		public ParamCondition(Condition condition, double borneMin, double borneMax, boolean bCanBeEmpty) {
			if (condition.name().contains("BETWEEN"))
				assert (!Double.isNaN(borneMin) && !Double.isNaN(borneMin)) : "Manque borne pour condition : " + condition;
			else
				assert (Double.isNaN(borneMin) && Double.isNaN(borneMin)) : "Bornes superflues pour condition : " + condition;
			// test additionnel pour les integer
			if (condition == Condition.INTEGER_BETWEEN)
				assert (double)((int) borneMin) == borneMin && (double)((int) borneMax) == borneMax : "Les bornes doivent êter des entiers pour condition : " + condition;
			
			this.condition = condition;
			this.borneMin = borneMin;
			this.borneMax = borneMax;
			this.type = getTypeParamFromTypeCondition(condition);
			this.bCanBeEmpty = bCanBeEmpty;
		}

		// Vérification de la validité de la valeur d'un paramètre, lue dans le fichier INI
		//  par rapport à la condition déclarée pour ce paramètre
		public String checkValide (String sValue) {

			//******************************************************************
			// Vérification sur le caractère vide ou non du paramètre
			if ((sValue==null || sValue.trim().length()==0) && !bCanBeEmpty)
				return "Le paramètre ne peut être vide";

			//******************************************************************
			// Vérification du type de paramètre
			if (type == Type.FLOAT && !Utils.isFloat(sValue))
				return "Le paramètre doit être un nombre flottant";
			if (type == Type.INTEGER && !Utils.isInteger(sValue))
				return "Le paramètre doit être un nombre entier";
			if (type == Type.COLOR && !isColor(sValue))
				return "Le paramètre doit être une couleur = nomCouleur  ou bien de la forme :  red, green, blue";

			//******************************************************************
			// Vérifications spécifiques
			double val;
			int val_i;
			
			switch (condition) {
				case STRING_ANY:
				case FLOAT_ANY:
				case INTEGER_ANY:
					return null; // déjà testé ci-dessus

				case BOOL:
					if (!sValue.equals("OUI") && !sValue.equals("NON"))
						return "Doit valoir OUI ou NON";
					break;

				case INTEGER_POS:
					if(!Utils.isInteger_Pos(sValue))
						return "Doit être un entier >= 0";
					break;

				case INTEGER_POS_STRICT:
					if(!Utils.isInteger_strictPos(sValue))
						return "Doit être un entier strictement >0";
					break;

				case INTEGER_BETWEEN:
					val_i = Integer.parseInt(sValue);
					if (val_i<borneMin || val_i>borneMax)
						return "Doit être un entier dans l'intervalle [" + borneMin + " , " + borneMax + "]";
					break;

				case FLOAT_POS:
					if(!Utils.isFloat_Pos(sValue))
						return "Doit être un flottant >= 0";
					break;

				case FLOAT_POS_STRICT:
					if(!Utils.isFloat_strictPos(sValue))
						return "Doit être un flottant strictement >0";
					break;
					
				case FLOAT_BETWEEN:
					val = Double.parseDouble(sValue);
					if (val<borneMin || val>borneMax)
						return "Doit être un flottant dans l'intervalle [" + borneMin + " , " + borneMax + "]";
					break;
					
				case FLOAT_BETWEEN_STRICT:
					val = Double.parseDouble(sValue);
					if (val<=borneMin || val>=borneMax)
						return "Doit être un flottant dans l'intervalle ]" + borneMin + " , " + borneMax + "[  (strictement)";
					break;

				case DIRECTORY_NAME_ABSOLUTE:
					if (!UtilsFile.isAbsoluteDirectoryNameValide(sValue))
						return "Ne constitue pas un nom de répertoire valide";
					break;

				case FILE_NAME:
					if (!UtilsFile.isFileNameValide(sValue))
						return "Ne constitue pas un nom de fichier valide";
					break;

				case FILE_EXTENSION:
					if (!UtilsFile.isExtensionValide(sValue))
						return "Ne constitue pas une extension de fichier valide (ne pas mettre de \'.\')";
					break;

				case FILE_EXISTANT:
					if (!new File(sValue).exists())
						return "Le fichier n'existe pas";
					break;

				case DIRECTORY_EXISTANT:
					if (!new File(sValue).exists())
						return "Le répertoire n'existe pas";
					break;

				case FILE_TEXT_CHARSET:
					if (!UtilsFile.isValideCharSet(sValue))
						return "Ce nom de système d'encodage de fichier n'est pas pris en charge";
					break;
					
				case LIST_VALUE:
					if (!listeValue.contains(sValue))
						return "Ce paramètre n'est pas dans la liste des valeurs possibles : " + listeValue.toString();
					break;

			}


			// Sinon tout est OK
			return null;
		}

		private Type getTypeParamFromTypeCondition(Condition condition) {
			switch (condition) {
				case FLOAT_ANY:
				case FLOAT_POS:
				case FLOAT_POS_STRICT:
				case FLOAT_BETWEEN:
				case FLOAT_BETWEEN_STRICT:
					return Type.FLOAT;

				case INTEGER_ANY:
				case INTEGER_POS:
				case INTEGER_POS_STRICT:
				case INTEGER_BETWEEN:
					return Type.INTEGER;

				case BOOL:
					return Type.BOOL;

				case STRING_ANY:
				case DIRECTORY_NAME_ABSOLUTE:
				case FILE_NAME:
				case FILE_EXTENSION:
				case FILE_EXISTANT:
				case DIRECTORY_EXISTANT:
				case FILE_TEXT_CHARSET:
				case LIST_VALUE:
					return Type.STRING;

				case COLOR:
					return Type.COLOR;

				default:
					assert(false) : "type de condition non prise en charge : " + condition;
					return null;
			}
		}
	}

	//**************************************************************************
	// ******* Fonctions utiles ************************************************
	//**************************************************************************
	private static boolean isColor(String s) {
		Color c = decodeColor(s);
		return c!=null;
	}
	private static Color decodeColor(String s) {
		if (s.trim().matches("[0-9]{1,3}\\s*,\\s*[0-9]{1,3}\\s*,\\s*[0-9]{1,3}")) {
			// la couleur est fournie sous forme de composante RGB
			String[] tabComposante = Utils.mySplit(s, ',');
			assert(tabComposante.length==3);

			int R = Integer.parseInt(tabComposante[0].trim());
			int G = Integer.parseInt(tabComposante[1].trim());
			int B = Integer.parseInt(tabComposante[2].trim());

			if (R>255 || G>255 || B>255)
				return null;

			return new Color(R, G, B);

		} else {
			// la couleur est désignée par son nom
			initMapColors();
			return mapColors.get(s.trim().toLowerCase().replace('_', ' '));
		}
	}

	private static HashMap<String, Color> mapColors = null;
	private static void initMapColors() {
		if (mapColors != null)
			return;
		
		// On essaie de ratisser large au niveau des noms de couleur ...
		mapColors = new HashMap<String, Color>();
		mapColors.put("noir", Color.BLACK);
		mapColors.put("black", Color.BLACK);

		mapColors.put("blanc", Color.WHITE);
		mapColors.put("white", Color.WHITE);

		mapColors.put("gris", Color.GRAY);
		mapColors.put("gray", Color.GRAY);

		mapColors.put("gris clair", Color.LIGHT_GRAY);
		mapColors.put("light_gray", Color.LIGHT_GRAY);

		mapColors.put("gris fonce", Color.DARK_GRAY);
		mapColors.put("gris foncé", Color.DARK_GRAY);
		mapColors.put("dark gray", Color.DARK_GRAY);

		mapColors.put("rouge", Color.RED);
		mapColors.put("red", Color.RED);

		mapColors.put("vert", Color.GREEN);
		mapColors.put("green", Color.GREEN);

		mapColors.put("bleu", Color.BLUE);
		mapColors.put("blue", Color.BLUE);

		mapColors.put("orange", Color.ORANGE);

		mapColors.put("jaune", Color.YELLOW);
		mapColors.put("yellow", Color.YELLOW);

		mapColors.put("rose", Color.PINK);
		mapColors.put("pink", Color.PINK);

		mapColors.put("cyan", Color.CYAN);
		
		mapColors.put("magenta", Color.MAGENTA);

	}
}
